﻿using System;
using System.Net;
using System.Net.Sockets;

namespace Org.Mentalis.Network.ProxySocket {

	/// <summary>
	/// References the callback method to be called when the protocol negotiation is completed.
	/// </summary>
	internal delegate void HandShakeComplete(Exception error);
	/// <summary>
	/// Implements a specific version of the SOCKS protocol. This is an abstract class; it must be inherited.
	/// </summary>
	internal abstract class SocksHandler {
		/// <summary>
		/// Initilizes a new instance of the SocksHandler class.
		/// </summary>
		/// <param name="server">The socket connection with the proxy server.</param>
		/// <param name="user">The username to use when authenticating with the server.</param>
		/// <exception cref="ArgumentNullException"><c>server</c> -or- <c>user</c> is null.</exception>
		public SocksHandler(Socket server, string user) {
			Server = server;
			Username = user;
		}
		/// <summary>
		/// Converts a port number to an array of bytes.
		/// </summary>
		/// <param name="port">The port to convert.</param>
		/// <returns>An array of two bytes that represents the specified port.</returns>
		protected byte[] PortToBytes(int port) {
			byte [] ret = new byte[2];
			ret[0] = (byte)(port / 256);
			ret[1] = (byte)(port % 256);
			return ret;
		}
		/// <summary>
		/// Converts an IP address to an array of bytes.
		/// </summary>
		/// <param name="address">The IP address to convert.</param>
		/// <returns>An array of four bytes that represents the specified IP address.</returns>
		protected byte[] AddressToBytes(long address) {
			byte [] ret = new byte[4];
			ret[0] = (byte)(address % 256);
			ret[1] = (byte)((address / 256) % 256);
			ret[2] = (byte)((address / 65536) % 256);
			ret[3] = (byte)(address / 16777216);
			return ret;
		}
		/// <summary>
		/// Reads a specified number of bytes from the Server socket.
		/// </summary>
		/// <param name="count">The number of bytes to return.</param>
		/// <returns>An array of bytes.</returns>
		/// <exception cref="ArgumentException">The number of bytes to read is invalid.</exception>
		/// <exception cref="SocketException">An operating system error occurs while accessing the Socket.</exception>
		/// <exception cref="ObjectDisposedException">The Socket has been closed.</exception>
		protected byte[] ReadBytes(int count) {
			if (count <= 0)
				throw new ArgumentException();
			byte[] buffer = new byte[count];
			int received = 0;
			while(received != count) {
				received += Server.Receive(buffer, received, count - received, SocketFlags.None);
			}
			return buffer;
		}
		/// <summary>
		/// Gets or sets the socket connection with the proxy server.
		/// </summary>
		/// <value>A Socket object that represents the connection with the proxy server.</value>
		/// <exception cref="ArgumentNullException">The specified value is null.</exception>
		protected Socket Server {
			get {
				return m_Server;
			}
			set {
				if (value == null)
					throw new ArgumentNullException();
				m_Server = value;
			}
		}
		/// <summary>
		/// Gets or sets the username to use when authenticating with the proxy server.
		/// </summary>
		/// <value>A string that holds the username to use when authenticating with the proxy server.</value>
		/// <exception cref="ArgumentNullException">The specified value is null.</exception>
		protected string Username {
			get {
				return m_Username;
			}
			set {
				if (value == null)
					throw new ArgumentNullException();
				m_Username = value;
			}
		}
		/// <summary>
		/// Gets or sets the return value of the BeginConnect call.
		/// </summary>
		/// <value>An IAsyncProxyResult object that is the return value of the BeginConnect call.</value>
		protected IAsyncProxyResult AsyncResult {
			get {
				return m_AsyncResult;
			}
			set {
				m_AsyncResult = value;
			}
		}
		/// <summary>
		/// Gets or sets a byte buffer.
		/// </summary>
		/// <value>An array of bytes.</value>
		protected byte[] Buffer {
			get {
				return m_Buffer;
			}
			set {
				m_Buffer = value;
			}
		}
		/// <summary>
		/// Gets or sets the number of bytes that have been received from the remote proxy server.
		/// </summary>
		/// <value>An integer that holds the number of bytes that have been received from the remote proxy server.</value>
		protected int Received {
			get {
				return m_Received;
			}
			set {
				m_Received = value;
			}
		}
		// private variables
		/// <summary>Holds the value of the Server property.</summary>
		private Socket m_Server;
		/// <summary>Holds the value of the Username property.</summary>
		private string m_Username;
		/// <summary>Holds the value of the AsyncResult property.</summary>
		private IAsyncProxyResult m_AsyncResult;
		/// <summary>Holds the value of the Buffer property.</summary>
		private byte[] m_Buffer;
		/// <summary>Holds the value of the Received property.</summary>
		private int m_Received;
		/// <summary>Holds the address of the method to call when the SOCKS protocol has been completed.</summary>
		protected HandShakeComplete ProtocolComplete;
		/// <summary>
		/// Starts negotiating with a SOCKS proxy server.
		/// </summary>
		/// <param name="host">The remote server to connect to.</param>
		/// <param name="port">The remote port to connect to.</param>
		public abstract void Negotiate(string host, int port);
		/// <summary>
		/// Starts negotiating with a SOCKS proxy server.
		/// </summary>
		/// <param name="remoteEP">The remote endpoint to connect to.</param>
		public abstract void Negotiate(IPEndPoint remoteEP);
		/// <summary>
		/// Starts negotiating asynchronously with a SOCKS proxy server.
		/// </summary>
		/// <param name="remoteEP">An IPEndPoint that represents the remote device. </param>
		/// <param name="callback">The method to call when the connection has been established.</param>
		/// <param name="proxyEndPoint">The IPEndPoint of the SOCKS proxy server.</param>
		/// <returns>An IAsyncProxyResult that references the asynchronous connection.</returns>
		public abstract IAsyncProxyResult BeginNegotiate(IPEndPoint remoteEP, HandShakeComplete callback, IPEndPoint proxyEndPoint);
		/// <summary>
		/// Starts negotiating asynchronously with a SOCKS proxy server.
		/// </summary>
		/// <param name="host">The remote server to connect to.</param>
		/// <param name="port">The remote port to connect to.</param>
		/// <param name="callback">The method to call when the connection has been established.</param>
		/// <param name="proxyEndPoint">The IPEndPoint of the SOCKS proxy server.</param>
		/// <returns>An IAsyncProxyResult that references the asynchronous connection.</returns>
		public abstract IAsyncProxyResult BeginNegotiate(string host, int port, HandShakeComplete callback, IPEndPoint proxyEndPoint);
	}
}